/*
 * Copyright 2009 Google Inc.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.pennychecker.eventbus;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.pennychecker.eventbus.Event.Type;

/**
 * Manager responsible for adding handlers to event sources and firing those
 * handlers on passed in events.
 */
public abstract class HandlerManager {
	/**
	 * Interface for queued add/remove operations.
	 */
	private interface AddOrRemoveCommand {

		void execute();
	}

	/**
	 * Inner class used to actually contain the handlers.
	 */
	private static class HandlerRegistry {

		private final HashMap<Event.Type<?>, ArrayList<?>> map = new HashMap<Event.Type<?>, ArrayList<?>>();

		private <H extends EventHandler> void addHandler(Type<H> type, H handler) {
			ArrayList<H> l = get(type);
			if (l == null) {
				l = new ArrayList<H>();
				map.put(type, l);
			}
			l.add(handler);
		}

		private <H extends EventHandler> void fireEvent(Event<H> event,
				boolean isReverseOrder) {
			Type<H> type = event.getAssociatedType();
			int count = getHandlerCount(type);
			if (isReverseOrder) {
				for (int i = count - 1; i >= 0; i--) {
					H handler = this.<H> getHandler(type, i);
					event.dispatch(handler);
				}
			} else {
				for (int i = 0; i < count; i++) {
					H handler = this.<H> getHandler(type, i);
					event.dispatch(handler);
				}
			}
		}

		@SuppressWarnings("unchecked")
		private <H> ArrayList<H> get(Event.Type<H> type) {
			// This cast is safe because we control the puts.
			return (ArrayList<H>) map.get(type);
		}

		private <H extends EventHandler> H getHandler(Event.Type<H> eventKey,
				int index) {
			ArrayList<H> l = get(eventKey);
			return l.get(index);
		}

		private int getHandlerCount(Event.Type<?> eventKey) {
			ArrayList<?> l = map.get(eventKey);
			return l == null ? 0 : l.size();
		}

		private boolean isEventHandled(Event.Type<?> eventKey) {
			return map.containsKey(eventKey);
		}

		private <H> void removeHandler(Event.Type<H> eventKey, H handler) {
			ArrayList<H> l = get(eventKey);
			boolean result = (l == null) ? false : l.remove(handler);
			if (result && l.size() == 0) {
				map.remove(eventKey);
			}
			assert result : "Tried to remove unknown handler: " + handler
					+ " from " + eventKey;
		}
	}

	private int firingDepth = 0;
	private boolean isReverseOrder;
	// map storing the actual handlers
	private HandlerRegistry registry;
	// source of the event.
	private final Object source;
	// Add and remove operations received during dispatch.
	private List<AddOrRemoveCommand> deferredDeltas;

	/**
	 * Creates a handler manager with the given source. Handlers will be fired
	 * in the order that they are added.
	 * 
	 * @param source
	 *            the event source
	 */
	public HandlerManager(Object source) {
		this(source, false);
	}

	/**
	 * Creates a handler manager with the given source, specifying the order in
	 * which handlers are fired.
	 * 
	 * @param source
	 *            the event source
	 * @param fireInReverseOrder
	 *            true to fire handlers in reverse order
	 */
	public HandlerManager(Object source, boolean fireInReverseOrder) {
		registry = new HandlerRegistry();
		this.source = source;
		this.isReverseOrder = fireInReverseOrder;
	}

	/**
	 * Adds a handle.
	 * 
	 * @param <H>
	 *            The type of handler
	 * @param type
	 *            the event type associated with this handler
	 * @param handler
	 *            the handler
	 * @return the handler registration, can be stored in order to remove the
	 *         handler later
	 */
	public <H extends EventHandler> HandlerRegistration addHandler(
			Event.Type<H> type, final H handler) {
		assert type != null : "Cannot add a handler with a null type";
		assert handler != null : "Cannot add a null handler";
		if (firingDepth > 0) {
			enqueueAdd(type, handler);
		} else {
			doAdd(type, handler);
		}

		return new DefaultHandlerRegistration(this, type, handler);
	}

	/**
	 * Fires the given event to the handlers listening to the event's type.
	 * 
	 * Note, any subclass should be very careful about overriding this method,
	 * as adds/removes of handlers will not be safe except within this
	 * implementation.
	 * 
	 * @param event
	 *            the event
	 */
	public void fireEvent(Event<?> event) {
		// If it not live we should revive it.
		if (!event.isLive()) {
			event.revive();
		}
		Object oldSource = event.getSource();
		event.setSource(source);
		try {
			firingDepth++;

			registry.fireEvent(event, isReverseOrder);

		} finally {
			firingDepth--;
			if (firingDepth == 0) {
				handleQueuedAddsAndRemoves();
			}
		}
		if (oldSource == null) {
			// This was my event, so I should kill it now that I'm done.
			event.kill();
		} else {
			// Restoring the source for the next handler to use.
			event.setSource(oldSource);
		}
	}

	/**
	 * Gets the handler at the given index.
	 * 
	 * @param <H>
	 *            the event handler type
	 * @param index
	 *            the index
	 * @param type
	 *            the handler's event type
	 * @return the given handler
	 */
	public <H extends EventHandler> H getHandler(Event.Type<H> type, int index) {
		assert index < getHandlerCount(type) : "handlers for "
				+ type.getClass() + " have size: " + getHandlerCount(type)
				+ " so do not have a handler at index: " + index;
		return registry.getHandler(type, index);
	}

	/**
	 * Gets the number of handlers listening to the event type.
	 * 
	 * @param type
	 *            the event type
	 * @return the number of registered handlers
	 */
	public int getHandlerCount(Type<?> type) {
		return registry.getHandlerCount(type);
	}

	/**
	 * Does this handler manager handle the given event type?
	 * 
	 * @param e
	 *            the event type
	 * @return whether the given event type is handled
	 */
	public boolean isEventHandled(Type<?> e) {
		return registry.isEventHandled(e);
	}

	/**
	 * Removes the given handler from the specified event type. Normally,
	 * applications should call {@link HandlerRegistration#removeHandler()}
	 * instead.
	 * 
	 * @param <H>
	 *            handler type
	 * 
	 * @param type
	 *            the event type
	 * @param handler
	 *            the handler
	 */
	public <H extends EventHandler> void removeHandler(Event.Type<H> type,
			final H handler) {
		if (firingDepth > 0) {
			enqueueRemove(type, handler);
		} else {
			doRemove(type, handler);
		}
	}

	/**
	 * Not part of the public API, available only to allow visualization tools
	 * to be developed in gwt-incubator.
	 * 
	 * @return a map of all handlers in this handler manager
	 */
	Map<Event.Type<?>, ArrayList<?>> createHandlerInfo() {
		return registry.map;
	}

	private void defer(AddOrRemoveCommand command) {
		if (deferredDeltas == null) {
			deferredDeltas = new ArrayList<AddOrRemoveCommand>();
		}
		deferredDeltas.add(command);
	}

	private <H extends EventHandler> void doAdd(Event.Type<H> type,
			final H handler) {
		registry.addHandler(type, handler);
	}

	private <H extends EventHandler> void doRemove(Event.Type<H> type,
			final H handler) {
		registry.removeHandler(type, handler);
	}

	private <H extends EventHandler> void enqueueAdd(final Event.Type<H> type,
			final H handler) {
		defer(new AddOrRemoveCommand() {

			public void execute() {
				doAdd(type, handler);
			}
		});
	}

	private <H extends EventHandler> void enqueueRemove(
			final Event.Type<H> type, final H handler) {
		defer(new AddOrRemoveCommand() {

			public void execute() {
				doRemove(type, handler);
			}
		});
	}

	private void handleQueuedAddsAndRemoves() {
		if (deferredDeltas != null) {
			try {
				for (AddOrRemoveCommand c : deferredDeltas) {
					c.execute();
				}
			} finally {
				deferredDeltas = null;
			}
		}
	}
}
